package org.oimwrapper.connectors;

import Thor.API.*;
import Thor.API.Operations.*;
import com.thortech.xl.dataobj.*;
import java.text.*;
import java.util.*;

/**
 * This is the basis for all recon tasks.<p>
 *
 * This task expects the following parameters.
 * <ul>
 * <li><code>CleanText</code> Setting this to <code>true<code> will remove all non-printable characters from the 
 *                            data submitted, and setting this to <code>false</code> will leave the submitted data
 *                            as-is.</li>
 * <li><code>DeleteOldRecords</code> Setting this to <code>true</code> will enable checking for accounts that
 *                                   exist in OIM but no longer exist on the target system.   Accounts not
 *                                   present will be revoked in OIM.   Setting this to <code>false</code>
 *                                   will disable these checks.</li>
 * <li><code>DeleteReconStatuses</code> If set, this will restrict delete recon to only consider accounts with
 *                                      the listed statuses (as a comma separated list) for deletion.   Objects
 *                                      in statuses other then those listed will not be deleted by the delete 
 *                                      recon pass.</li>
 * <li><code>DemoMode</code> Setting this to <code>true</code> will only process the data and print the data 
 *                           to the debug logs.   No recon events will be submitted to OIM.  Setting this to 
 *                           <code>false</code> will submit recon events to OIM as is normally done.</li>
 * <li><code>EmailErrorTo</code> If present, this should be a comma separated list of email addresses to recieve
 *                               a notification email for each submit error ecounter during the job.</li>
 * <li><code>EmailFrom</code> The email address that notifications will for the form address.   If this field is 
 *                            not present, no email notifications will be sent.</li>
 * <li><code>EmailStatusTo</code> If present, this should be a comma separated list of email addresses to recieve
 *                                a summary message upon completion of the job.</li>
 * <li><code>HandleEmptyChildTables</code> Setting this to <code>true</code> will submit child data in such that
 *                                         child data will be cleared out if it is removed from the target system, 
 *                                         or setting this to <code>false</code> will not clear out child data if
 *                                         all the entries are removed on the target system.  Setting this to 
 *                                         <code>false</code> is required for working around OIM bug #5894269.
 * <li><code>IgnoredChars</code> A list of characters to be removed from all text attributes.
 *                          i.e. <code>;-"'</code></li>
 * <li><code>ResourceObject</code> The name of the resource object that this task will submit reconciliation
 *                                 event against.</li>
 * <li><code>StaticAttributeLookup</code> The name of a lookup table containing a set of static attributes and 
 *                                        values to submit as part of every recon event.</li>
 * <li><code>SubmitAllRecords</code> Setting this to <code>true</code> will submit all reconciliation events
 *                                   for processing, setting this to <code>false</code> will only submit 
 *                                   events in which data changes are detected.   Events in which no data
 *                                   differences can be found between the target system and OIM will not be
 *                                   submitted.</li>
 * <li><code>Threads</code> The number of concurrent threads to run to submit events to OIM.  By default, only
 *                          one submitting thread is run.</li>
 * <li><code>UseJMS</code> <code>true</code> to submit recon events to JMS, or <code>false</code> to 
 *                         process recon events directly.</li>
 * </ul>
 */
public abstract class OIMReconTaskBase extends OIMTaskBase {

	/**
	 * The recon operations instance used by this task.
	 */
	private tcReconciliationOperationsIntf reconOp;

	/**
	 * True if a stop has been requested for this task.
	 */
	private boolean stop;

	/**
	 * True if the ignoreEvent() api should not be called, and instead all records should be submitted
	 * for recon, even ones that haven't changed.
	 */
	private boolean reconAll;

	/**
	 * The name of the resource object in which this task is being run against.
	 */
	private String resourceObject;

	/**
	 * True if resources instances on OIM no longer present on the target system should be removed.
	 */
	private boolean deleteRecon;

	/**
	 * True if multi-valued attribute lookup should be preformed.
	 */
	private boolean doMultiValuedRecon;

	/**
	 * Indicates that text fields should be normalized to remove extra whitepace and invisible chracters.
	 */
	private boolean cleanText;
	
	/**
	 * Indicates that recon events should be processed for changes, but no events should be submitted.
	 */
	private boolean demoMode;
	
	/**
	 * Indicates that empty child tables will be submitted as such in recon events.
	 */
	private boolean handleEmptyChildTables;
	
	/**
	 * The address that email notices from this should be sent to in the case of an error on a record. 
	 */
	private String emailErrorTo;
	
	/**
	 * The address the email notices from this task should be sent from.
	 */
	private String emailFrom;
	
	/**
	 * The lowest recon event id submitted.
	 */
	private long minID;
	
	/**
	 * The highest recon event id submitted.
	 */
	private long maxID;
		
	/**
	 * The number of failed records.
	 */
	private int failedRecords;
	
	/**
	 * A Map of Lists of Maps of parent data to use for deleted account detection.
	 */
	private Map deletionCheckData;
	
	/**
	 * <code>true</code> to submit recon events to JMS, or <code>false</code> to process recon events directly.
	 */
	private boolean useJms;
	
	/**
	 * Contains a map of resource names and the sets of multi-valued attributes used by those 
	 * resources in OIM.
	 */
	private Map resourceMultiAttrs;
	
	/**
	 * Contains a map of resource names and the sets of key attributes used by those 
	 * resources in OIM.
	 */
	private Map resourceKeyAttrs;
	
	/**
	 * The date format used by the recon engine.
	 */
	private String dateFormat;
	
	/**
	 * Characters to be removed from the record results.
	 */
	private String ignoredChars;
    
	/**
	 * List of String attributes for the resource
	 */
	private Map fieldsList;
        
	/**
	 * A map of static single value attributes to submit with every event.
	 */
	protected Map staticAttributes;
	
	/**
	 * This method is called once per task, and should initialize and open a connection to the target system.
	 * 
	 * @exception Exception
	 */
	public abstract void openConnection() throws Exception;

	/**
	 * This method is called once per task, and should close the open connection to the target system.
	 *
	 * @exception Exception
	 */
	public abstract void closeConnection() throws Exception;

	/**
	 * This method is called once to retrieve a List of records on the target system.   The objects in this 
	 * list are consumed by the <code>getSingleValuedData()</code> and <code>getMultiValuedData()</code>
	 * methods, and not directly used by any other methods.
	 * 
	 * @return A List of records on the target system.
	 * @exception Exception
	 */
	public abstract List getRecords() throws Exception;

	/**
	 * This method is called to convert the single-valued data in the arbitrary Record Object to a Map
	 * containing attribute names as the key, and the corresponding attribute value as the value.
	 * 
	 * @param Record An arbitrary object that contains the data for a single event.
	 * @return A Map containing the attribute value pairs from the record for all single-valued attributes.
	 * @exception Exception
	 */
	public abstract Map getSingleValuedData(Object Record) throws Exception;

	/**
	 * This method is called to convert the multi-valued data in the arbitrary Record Object to a Map
	 * containing attribute names as the key, and an Array of Maps for the value.  The Maps in the 
	 * value should contain attribute value pairs for each child record, and one Map should be present
	 * in the array for each child element.
	 * 
	 * @param Record An arbitrary object that contains the data for a single event.
	 * @return A Map containing the attribute value pairs from the record for all multi-valued attributes.
	 * @exception Exception
	 */
	public abstract Map getMultiValuedData(Object Record) throws Exception;

	/**
	 * The default constructor.
	 * 
	 * @param LogCategory The name of the category to log messages to.
	 */
	public OIMReconTaskBase(String LogCategory) {
		super(LogCategory);
		stop = false;
		doMultiValuedRecon = true;
		resourceMultiAttrs = new HashMap();
		resourceKeyAttrs = new HashMap();
	}

	/**
	 * This method is called by the scheduler to stop a running task instance.
	 * 
	 * @return This always returns <code>true</code>.
	 */
	public final boolean stop() {
		logger.debug("Entering reconTaskBase.stop()");
		logger.warn(" ---- Stopping current task ---- ");
		stop = true;
		logger.debug("Exiting reconTaskBase.stop()");
		return stop;
	}

	/**
	 * This method returns a boolean indicating whether or not a stop has been requested.
	 *
	 * @return <code>true</code> if a stop is requested, or <code>false</code> otherwise.
	 */
	public final boolean isStopped() {
		return stop;
	}

	/**
	 * This mathod is called by the scheduler to execute the task instance.
	 * 
	 */
	public final void execute() {
		Date start = new Date();
		logger.debug("Entering reconTaskBase.execute()");
		try {
			minID = 0;
			maxID = 0;
			int recordCount = 0;
			failedRecords = 0;
			reconOp = (tcReconciliationOperationsIntf) getUtility(tcReconciliationOperationsIntf.class);
			dateFormat = tcRCD.getDateStorageFormat();
			staticAttributes = getLookupValues(getAttribute("StaticAttributeLookup"));
			reconAll = getBooleanAttribute("SubmitAllRecords", false);
			deleteRecon = getBooleanAttribute("DeleteOldRecords", false);
			resourceObject = getDefaultAttribute("ResourceObject", getClass().getName());
			String emailStatusTo = getAttribute("EmailStatusTo");
			emailErrorTo = getAttribute("EmailErrorTo");
			emailFrom = getAttribute("EmailFrom");
			cleanText = getBooleanAttribute("CleanText", false);
			demoMode = getBooleanAttribute("DemoMode", false);
			handleEmptyChildTables = getBooleanAttribute("HandleEmptyChildTables", true);
			useJms = getBooleanAttribute("UseJMS", true);
			ignoredChars = getDefaultAttribute("IgnoredChars", "");
			fieldsList = new HashMap();
			logger.info("---------------------------------------------------------");
			logger.info(" Beginning Reconciliation For " + resourceObject);
			logger.info("---------------------------------------------------------");
			openConnection();
			List records = getRecords();
			String size = Integer.toString(records.size());
			deletionCheckData = new HashMap();
			int maxWorkers = Integer.parseInt(getDefaultAttribute("Threads", "1"));
			for (Iterator iterator = records.iterator(); !stop && iterator.hasNext(); recordCount++) {
				if (recordCount == 0 || (recordCount + 1) % 100 == 0) {
					logger.info("[" + resourceObject + "] Processing " + Integer.toString(recordCount + 1) + " of " + size);
				} else {
					logger.debug("[" + resourceObject + "] Processing " + Integer.toString(recordCount + 1) + " of " + size);
				}
				startThread(maxWorkers, new worker(this, iterator.next()));
			}
			waitForAllThreads();
			int deletedCount = 0;
			if (!stop && deleteRecon) {
				logger.debug("Performing deletion reconciliation");
				try {
					for (Iterator iterator = deletionCheckData.keySet().iterator(); iterator.hasNext(); ) {
						String resource = (String) iterator.next();
						logger.debug("Checking " + resource + " for deleted objects");
						List data = (List) deletionCheckData.get(resource);
						logger.debug("Checking " + Integer.toString(data.size()) + " objects for deletion");
						if (!stop) {
							Set matchedAccounts = new HashSet();
							if (data.size() > 0) {
								Map[] deleteCheckArray = prepareDeletetionDetectionData(resource, data);
								matchedAccounts = reconOp.provideDeletionDetectionData(resource, deleteCheckArray);
							}
							matchedAccounts.addAll(getAccountsToSkip(resource));
							tcResultSet resourcesToRemove = reconOp.getMissingAccounts(resource, matchedAccounts);
							if (logger.isDebugEnabled() && Arrays.asList(resourcesToRemove.getColumnNames()).contains("Process Instance.Key")) {
								for (int i = 0; i < resourcesToRemove.getRowCount(); i++) {
									resourcesToRemove.goToRow(i);
									long processInstanceKey = resourcesToRemove.getLongValue("Process Instance.Key");
									logger.debug("Generating delete recon event for " + Long.toString(processInstanceKey));
									printDebugDump(getProcessFormValues(processInstanceKey));
								}
							}
							if (!stop && !resourcesToRemove.isEmpty() && !demoMode) {
								long[] events = reconOp.deleteDetectedAccounts(resourcesToRemove);
								deletedCount = events.length;
								logger.info("Submitted " + Integer.toString(events.length) + " events to delete resources");
								for (int i = 0; i < events.length; i++) {
									tallySuccess(events[i]);
								}
							}
						}
					}
				} catch (Exception e) {
					OIMExceptionHandler.handleException(logger, e, getClass());
					failedRecords++;
					try {
						if (!isEmpty(emailErrorTo) && !isEmpty(emailFrom)) {
							String body = "An error occured processing the deletion events for " + resourceObject;
							body += " - " + e.getClass().getName() + " : " + e.toString();
							sendEmail("Reconcilation failure for " + resourceObject, body, emailFrom, emailErrorTo);
						}
					} catch (Exception f) {
						OIMExceptionHandler.handleException(logger, f, getClass());
					}
				}
				logger.debug("Completed deletion reconciliation");
			}
			closeConnection();
			Date end = new Date();
			long seconds = (end.getTime() - start.getTime()) / 1000;
			double rate = 0.0;
			if (seconds > 0) {
				rate = recordCount / seconds;
			}
			long hours = seconds / 3600;
			seconds = seconds - (hours * 3600);
			long minutes = seconds / 60;
			seconds = seconds - (minutes * 60);
			String difference = Long.toString(hours) + ":";
			if (minutes < 10) {
				difference += "0";
			}
			difference += Long.toString(minutes) + ":";
			if (seconds < 10) {
				difference += "0";
			}
			difference += Long.toString(seconds);
			logger.info("---------------------------------------------------------");
			logger.info(" Completed Reconciliation For " + resourceObject);
			logger.info("");
			logger.info("   Processed Records: " + Integer.toString(recordCount));
			logger.info("      Failed Records: " + Integer.toString(failedRecords));
			logger.info("     Deleted Records: " + Integer.toString(deletedCount));
			logger.info("  Records per Second: " + Double.toString(rate));
			logger.info("      Event ID Range: " + Long.toString(minID) + " - " + Long.toString(maxID));
			logger.info("        Elapsed Time: " + difference);
			logger.info("---------------------------------------------------------");
			if (!isEmpty(emailStatusTo) && !isEmpty(emailFrom)) {
				String body = " Completed Reconciliation For " + resourceObject + "\n\n";
				body += "   Processed Records: " + Integer.toString(recordCount) + "\n";
				body += "      Failed Records: " + Integer.toString(failedRecords) + "\n";
				body += "     Deleted Records: " + Integer.toString(deletedCount) + "\n";
				body += "  Records per Second: " + Double.toString(rate) + "\n";
				body += "      Event ID Range: " + Long.toString(minID) + " - " + Long.toString(maxID) + "\n";
				body += "        Elapsed Time: " + difference + "\n";
				sendEmail("Reconcilation completed for " + resourceObject, body, emailFrom, emailStatusTo);
			}
		} catch (Exception e) {
			OIMExceptionHandler.handleException(logger, e, getClass());
		}
		logger.debug("Exiting reconTaskBase.execute()");
	}
	
	/**
	 * This function can be overrided to allow for recon events to be submitted against different
	 * object types.
	 * 
	 * @param Record The record currently being processed.
	 * @return The name of the resource object to submit the event against.
	 * @exception Exception
	 */
	public String getResourceName(Object Record) throws Exception {
		return getCriticalAttribute("ResourceObject");
	}
	
	/**
	 * This method checks to see a multi-valued field is actually configured on a resource object
	 * before attempting to recon data against that field.  This only works on OIM 9.1 and later.
	 * Earlier versions of OIM will alwyas return true as they do not have an API to check field
	 * presence.
	 * 
	 * @param Resource The name of the resource to check the field existence against.
	 * @param Attribute The name of the attribute to check the field existence for.
	 * @return <code>true</code> if the field exists, or <code>false</code> if it does not.
	 * @exception Exception
	 */
	public final boolean multiValueConfigured(String Resource, String Attribute) throws Exception {
		logger.debug("Entering reconTaskBase.multiValueConfigured()");
		Map attributes = (Map) resourceMultiAttrs.get(Resource);
		if (attributes == null) {
			attributes = new HashMap();
			resourceMultiAttrs.put(Resource, attributes);
		}
		logger.debug("Checking for child attribute " + Attribute + " in " + Resource);
		Boolean result = (Boolean) attributes.get(Attribute);
		if (result == null) {
			if (checkIfOimVersion("9.1")) {
				tcObjectOperationsIntf objectOp = (tcObjectOperationsIntf) getUtility(tcObjectOperationsIntf.class);
				tcResultSet fields = objectOp.getReconciliationFields(getResourceObject(Resource));
				for (int i = 0; i < fields.getRowCount(); i++) {
					fields.goToRow(i);
					boolean value = fields.getStringValue("Objects.Reconciliation Fields.Type").equalsIgnoreCase("Multi-Valued");
					String field = fields.getStringValue("Objects.Reconciliation Fields.Name");
					Boolean existing = (Boolean) attributes.get(field);
					if (existing != null) {
						value |= existing.booleanValue();
					}
					attributes.put(field, new Boolean(value));
				}
				result = (Boolean) attributes.get(Attribute);
				if (result == null) {
					result = new Boolean(false);
					attributes.put(Attribute, result);
				}
			} else {
				result = new Boolean(true);
				attributes.put(Attribute, result);
			}
		}
		logger.debug("attributes = " + attributes.toString());
		logger.debug("Exiting reconTaskBase.multiValueConfigured()");
		return result.booleanValue();
	}

	/**
	 * Disables multi-valued attribute recon.
	 */
	public final void disableMultiValuedRecon() {
		logger.debug("Entering reconTaskBase.disableMultiValuedRecon()");
		doMultiValuedRecon = false;
		logger.debug("Entering reconTaskBase.disableMultiValuedRecon()");
	}
	
	/**
	 * Disables delete recon.
	 */
	public final void disableDeleteRecon() {
		logger.debug("Entering reconTaskBase.disableDeleteRecon()");
		deleteRecon = false;
		logger.debug("Entering reconTaskBase.disableDeleteRecon()");
	}

	/**
	 * Formats a date value for recon.
	 * 
	 * @param Input The data to format.
	 * @return A formated date string.
	 * @exception Exception
	 */
	public final String formatDate(Date Input) throws Exception {
		logger.debug("Entering reconTaskBase.formatDate()");
		String result = null;
		Date input = Input == null ? new Date(0) : Input;
		if (checkIfOimVersion("9.1.0.1862.4") && input.getTime() == 0) {
			result = "";
		} else {
			result = new SimpleDateFormat(dateFormat).format(input);
		}
		logger.debug("Exiting reconTaskBase.formatDate()");
		return result;
	}
	
	/**
	 * Helper function to start the multi-valued attribute Map.   Existing values for the
	 * multi-valued attribute are cleared when this function is called.
	 * 
	 * @param Destination The destination Map that will hold all the multi-valued records.
	 * @param Attribute The name of the multi-valued attribute to store the records under.
	 */
	public final void startMultiValuedRecords(Map Destination, String Attribute) {
		logger.debug("Entering reconTaskBase.startMultiValuedRecords()");
		Destination.put(Attribute, new Map[0]);
		logger.debug("Exiting reconTaskBase.startMultiValuedRecords()");
	}
	
	/**
	 * Helper function to build the multi-valued attribute Map, one record at a time.
	 * 
	 * @param Destination The destination Map that will hold all the multi-valued records.
	 * @param Attribute The name of the multi-valued attribute to store the record under.
	 * @param Record A single child table record to store.
	 */
	public final void addMultiValuedRecord(Map Destination, String Attribute, Map Record) {
		logger.debug("Entering reconTaskBase.addMultiValuedRecord()");
		Map[] records = (Map[]) Destination.get(Attribute);
		if (records == null) {
			records = new Map[0];
		}
		List recordList = new ArrayList(Arrays.asList(records));
		recordList.add(Record);
		Destination.put(Attribute, (Map[]) recordList.toArray(new Map[recordList.size()]));
		logger.debug("Exiting reconTaskBase.addMultiValuedRecord()");
	}
	
	/**
	 * Finds accounts that should not be considered for deletion based on their status.
	 * 
	 * @return A set of process instance keys of accounts to skip.
	 * @exception Exception
	 */
	protected Set getAccountsToSkip(String Resource) throws Exception {
		logger.debug("Entering reconTaskBase.getAccountsToSkip()");
		Set result = new HashSet();
		String deleteReconStatuses = getDefaultAttribute("DeleteReconStatuses", "");
		if (deleteReconStatuses.length() > 0) {
			String orderedFor = getResourceAttribute(Resource, "Objects.Order For");
			String query = null;
			if (orderedFor.equalsIgnoreCase("U")) {
				query = "select orc.orc_key, ost_status from orc inner join oiu on orc.orc_key = oiu.orc_key inner join ost on oiu.ost_key = ost.ost_key where orc_status not in ('X') and ost_status not in ";
			} else if (orderedFor.equalsIgnoreCase("O")) {
				query = "select orc.orc_key, ost_status from orc inner join oio on orc.orc_key = oio.orc_key inner join ost on oio.ost_key = ost.ost_key where orc_status not in ('X') and ost_status not in ";
			}
			if (query != null) {
				query += "('" + deleteReconStatuses.replaceAll(",", "','") + "')";
				Map[] instances = getOimData(query);
				for (int i = 0; i < instances.length; i++) {
					Long exclude = Long.valueOf((String) instances[i].get("Process Instance.Key"));
					logger.debug("Excluding: " + exclude.toString() + ", status is " + (String) instances[i].get("Objects.Object Status.Status"));
					result.add(exclude);
				}
			}
		}
		logger.debug("Exiting reconTaskBase.getAccountsToSkip()");
		return result;
	}
	
	/**
	 * Resolves IT resource names to keys in the deletion detection data.
	 * 
	 * @param Resource The name of the resource that the data is for.
	 * @param Data The data for deletion detection.
	 * @return The processed data.
	 * @throws java.lang.Exception
	 */
	public Map[] prepareDeletetionDetectionData(String Resource, List Data) throws Exception {
		logger.debug("Entering reconTaskBase.prepareDeletetionDetectionData()");
		Set itResourceFields = new HashSet();
		Map itResources = new HashMap();
		String query = "select orf_fieldname from orf ";
		query += "inner join obj on orf.obj_key = obj.obj_key ";
		query += "where orf_fieldtype='IT Resource' ";
		query += "and obj_name = '" + Resource + "'";
		logger.debug("query = " + query);
		Map[] queryResults = getOimData(query);
		for (int i = 0; i < queryResults.length; i++) {
			itResourceFields.add(queryResults[i].get("Objects.Reconciliation Fields.Name"));
		}
		Map[] result = new Map[Data.size()];
		for (int i = 0; i < Data.size(); i++) {
			Map item = (Map) Data.get(i);
			result[i] = new HashMap();
			for (Iterator iterator = item.keySet().iterator(); iterator.hasNext(); ) {
				String attribute = (String) iterator.next();
				String value = (String) item.get(attribute);
				if (itResourceFields.contains(attribute)) {
					String itResourceKey = (String) itResources.get(value);
					if (itResourceKey == null) {
						itResourceKey = Long.toString(getItResource(value));
						itResources.put(value, itResourceKey);
					}
					value = itResourceKey;
				}
				result[i].put(attribute, value);
			}
		}
		logger.debug("Exiting reconTaskBase.prepareDeletetionDetectionData()");
		return result;
	}
	
	/**
	 * Submits parent data for use in delete reconciliation.
	 * 
	 * @param ParentData A Map of the parent data to use for delete recon.
	 * @exception Exception
	 */
	private synchronized void addParentData(String Resource, Map ParentData) throws Exception {
		logger.debug("Entering reconTaskBase.addParentData()");
		List data = (List) deletionCheckData.get(Resource);
		if (data == null) {
			data = new ArrayList();
			deletionCheckData.put(Resource, data);
		}
		if (checkIfOimVersion("9.1")) {
			List attributes = (List) resourceKeyAttrs.get(Resource);
			if (attributes == null) {
				attributes = new ArrayList();
				tcObjectOperationsIntf objectOp = (tcObjectOperationsIntf) getUtility(tcObjectOperationsIntf.class);
				tcFormDefinitionOperationsIntf formDefOp = (tcFormDefinitionOperationsIntf) getUtility(tcFormDefinitionOperationsIntf.class);
				tcResultSet processes = objectOp.getProcessesForObject(getResourceObject(Resource));
				for (int i = 0; i < processes.getRowCount(); i++) {
					processes.goToRow(i);
					if (processes.getStringValue("Process Definition.Default Process").equalsIgnoreCase("1")) {
						long processDefKey = processes.getLongValue("Process Definition.Key");
						tcResultSet fields = formDefOp.getReconDataFlowForProcess(processDefKey);
						for (int j = 0; j < fields.getRowCount(); j++) {
							fields.goToRow(j);
							if (fields.getStringValue("Process Definition.Reconciliation Fields Mappings.Iskey").equalsIgnoreCase("1")) {
								attributes.add(fields.getStringValue("Objects.Reconciliation Fields.Name"));
							}
						}
						break;
					}
				}
				if (attributes.size() > 0) {
					resourceKeyAttrs.put(Resource, attributes);
				}
			}
			Map keysOnly = new HashMap();
			if (attributes.size() > 0) {
				for (Iterator iterator = attributes.iterator(); iterator.hasNext(); ) {
					String attribute = (String) iterator.next();
					if (ParentData.get(attribute) != null) {
						keysOnly.put(attribute, ParentData.get(attribute));
					}
				}
				data.add(keysOnly);
			} else {
				data.add(ParentData);
			}
		} else {
			data.add(ParentData);
		}
		logger.debug("Exiting reconTaskBase.addParentData()");
	}
	
	/**
	 * Updates the successful record stats.
	 * 
	 * @param EventID The recon event id of the successful record.
	 */
	protected synchronized void tallySuccess(long EventID) {
		logger.debug("Entering reconTaskBase.tallySuccess()");
		if (minID == 0 || EventID < minID) {
			minID = EventID;
		}
		if (maxID == 0 || EventID > maxID) {
			maxID = EventID;
		}
		logger.debug("Exiting reconTaskBase.tallySuccess()");
	}
	
	/**
	 * Updates the failed record stats.
	 */
	protected synchronized void tallyFailure() {
		logger.debug("Entering reconTaskBase.tallyFailure()");
		failedRecords++;
		logger.debug("Exiting reconTaskBase.tallyFailure()");
	}
	
	/**
	 * Submits a single record as a recon event.
	 * 
	 * @param Record The record to submit.
	 */
	private void processSingleRecord(Object Record) {
		logger.debug("Entering reconTaskBase.processSingleRecord()");
		Map reconSingleData = null;
		try {
			String resource = getResourceName(Record);
			reconSingleData = getSingleValuedData(Record);
			reconSingleData.putAll(staticAttributes);
			if (cleanText) {
				reconSingleData = cleanCharacters(reconSingleData);
			}
			if (ignoredChars.length() > 0) {
				reconSingleData = ignoreCharacters(resource, reconSingleData);
			}
			if (deleteRecon) {
				addParentData(resource, reconSingleData);
			}
			Map reconMultiData = getMultiValuedData(Record);
			boolean ignoreEvent = !reconAll;
			if (ignoreEvent) {
				ignoreEvent = reconOp.ignoreEvent(resource, reconSingleData, dateFormat);
				logger.debug("ignoreEvent() returned " + Boolean.toString(ignoreEvent));
				for (Iterator keys = reconMultiData.keySet().iterator(); doMultiValuedRecon && ignoreEvent && keys.hasNext(); ) {
					String multiValuedField = (String) keys.next();
					if (!multiValueConfigured(resource, multiValuedField)) {
						logger.debug("Multi-valued field " + multiValuedField + " is not configured on " + resource + "; skipping this field");
					} else {
						Map[] childValues = (Map[]) reconMultiData.get(multiValuedField);
						for (int i = 0; cleanText && i < childValues.length; i++) {
							childValues[i] = cleanCharacters(childValues[i]);
						}
						for (int i = 0; ignoredChars.length() > 0 && i < childValues.length; i++) {
							childValues[i] = ignoreCharacters(resource, childValues[i]);
						}
						ignoreEvent = reconOp.ignoreEventAttributeData(resource, reconSingleData, multiValuedField, childValues, dateFormat);
						logger.debug("ignoreEventAttributeData() [" + multiValuedField + "] returned " + Boolean.toString(ignoreEvent));
					}
				}
			}
			if (ignoreEvent) {
				logger.debug("Reconciliation is not needed for this record; no differences detected");
			} else {
				printDebugDump(reconSingleData);
				if (!demoMode) {
					long eventID = reconOp.createReconciliationEvent(resource, reconSingleData, false, dateFormat);
					logger.debug("Created reconciliation event " + Long.toString(eventID));
					for (Iterator keys = reconMultiData.keySet().iterator(); doMultiValuedRecon && keys.hasNext(); ) {
						String multiValuedField = (String) keys.next();
						if (multiValueConfigured(resource, multiValuedField)) {
							Map[] childValues = (Map[]) reconMultiData.get(multiValuedField);
							printDebugDump(childValues);
							for (int i = 0; i < childValues.length; i++) {
								reconOp.addMultiAttributeData(eventID, multiValuedField, childValues[i], dateFormat);
							}
							if (handleEmptyChildTables || childValues.length > 0) {
								reconOp.providingAllMultiAttributeData(eventID, multiValuedField, true);
							}
						}
					}
					if (useJms) {
						reconOp.finishReconciliationEvent(eventID);
						logger.debug("Submited reconciliation event " + Long.toString(eventID));
					} else {
						reconOp.processReconciliationEvent(eventID);
						logger.debug("Processed reconciliation event " + Long.toString(eventID));
					}
					tallySuccess(eventID);
				}
			}
		} catch (Exception e) {
			OIMExceptionHandler.handleException(logger, e, getClass());
			tallyFailure();
			try {
				if (!isEmpty(emailErrorTo) && !isEmpty(emailFrom)) {
					String body = "An error occured processing the reconcilation event for " + resourceObject;
					body += " - " + e.getClass().getName() + " : " + e.toString() + "\r\n\r\n";
					body += "The failed event had the following data: \r\n";
					if (reconSingleData == null) {
						body += "(null)";
					} else {
						for (Iterator jterator = reconSingleData.keySet().iterator(); jterator.hasNext(); ) {
							String attribute = (String) jterator.next();
							String value = (String) reconSingleData.get(attribute);
							body += " {" + attribute + " = " + value + "}";
						}
					}
					sendEmail("Reconcilation failure for " + resourceObject, body, emailFrom, emailErrorTo);
				}
			} catch (Exception f) {
				OIMExceptionHandler.handleException(logger, f, getClass());
			}
		}
		logger.debug("Exiting reconTaskBase.processSingleRecord()");
	}
	
	/**
	 * Returns a list of String attributes from the resource object
	 * @param Resource Resource name
	 * @return Set list of fields that are identified as Strings
	 * @exception Exception
	 */
	protected final Set getTextFields(String Resource) throws Exception {
		logger.debug("Entering reconTaskBase.getTextFields()");
		Set result = (Set) fieldsList.get(Resource);
		if (result == null) {
			synchronized (fieldsList) {
				result = new HashSet();
				tcObjectOperationsIntf objectOp = (tcObjectOperationsIntf) getUtility(tcObjectOperationsIntf.class);
				tcResultSet fields = objectOp.getReconciliationFields(getResourceObject( Resource ));
				for (int i = 0; i < fields.getRowCount(); i++) {
					fields.goToRow(i);
					if (fields.getStringValue("Objects.Reconciliation Fields.Type").equalsIgnoreCase("String")) {
						result.add(fields.getStringValue("Objects.Reconciliation Fields.Name"));
					}
				}
				fieldsList.put(Resource, result);
			}
		}
		logger.debug("Exiting reconTaskBase.getTextFields()");
		return result;
	}
        
	/**
	 * Removes invalid characters from a string that could cause problems (comma, semi-colon, whitespace, etc.)
	 * @param Value The string to process
	 * @return String the resulting parsed string
	 * @exception Exception
	 */
	protected final String removeIgnoredCharacters( String Value ) throws Exception {
		logger.debug("Entering reconTaskBase.removeIgnoredCharacters()");
		String result = Value;
		char[] chars = ignoredChars.toCharArray();
		for (int i = 0; i < chars.length; i++) {
			result = result.replaceAll("\\" + Character.toString(chars[i]), "");
		}
		logger.debug("Exiting reconTaskBase.removeIgnoredCharacters()");
		return result;
	}
        
	/**
	 * Removes non-printable characters for a Map of recon data.
	 *
	 * @param Data A Map containing attribute-value pairs to submit for reconciliation.
	 * @return A cleaned Map with all the control characters removed.
	 * @exception Exception
	 */
	protected final Map ignoreCharacters(String Resource, Map Data) throws Exception {
		logger.debug("Entering reconTaskBase.ignoreCharacters()");
		Map result = new HashMap();
		for (Iterator iterator = Data.keySet().iterator(); iterator.hasNext(); ) {
			String key = (String) iterator.next();
			String value = (String) Data.get(key);
			if (value != null) {
				if (!checkIfOimVersion("9.1") || getTextFields(Resource).contains(key)) {
					value = removeIgnoredCharacters( value );
				}
			}
			result.put(key, value);
		}
		logger.debug("Exiting reconTaskBase.ignoreCharacters()");
		return result;
	}
	
	/**
	 * Worker Thread.
	 */
	private class worker extends OIMTaskThreadBase {
		
		/**
		 * The invoking recon task.
		 */
		private OIMReconTaskBase reconTask;
		
		/**
		 * The record to process.
		 */
		private Object record;
		
		/**
		 * Default Constructor.
		 * 
		 * @param ReconTask The invoking recon task.
		 * @param Record The record to process.
		 */
		private worker(OIMReconTaskBase ReconTask, Object Record) {
			super(ReconTask);
			reconTask = ReconTask;
			record = Record;
		}
		
		/**
		 * Processes and submits a single recon event.
		 */
		protected void runOimThread() {
			reconTask.processSingleRecord(record);
		}
	}
}